/*
 * NotesView.java
 * 
 * Copyright (c) 2007
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.dcarew.notes.views;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.dcarew.notes.NotesFileUtils;
import org.dcarew.notes.NotesPlugin;
import org.eclipse.core.commands.operations.IUndoContext;
import org.eclipse.core.runtime.IPath;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IMenuListener;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.text.DefaultTextDoubleClickStrategy;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.jface.text.IFindReplaceTarget;
import org.eclipse.jface.text.IRewriteTarget;
import org.eclipse.jface.text.ITextOperationTarget;
import org.eclipse.jface.text.ITextViewerExtension;
import org.eclipse.jface.text.IUndoManagerExtension;
import org.eclipse.jface.text.TextViewer;
import org.eclipse.jface.text.TextViewerUndoManager;
import org.eclipse.jface.text.hyperlink.DefaultHyperlinkPresenter;
import org.eclipse.jface.text.hyperlink.IHyperlinkDetector;
import org.eclipse.jface.text.hyperlink.URLHyperlinkDetector;
import org.eclipse.jface.text.presentation.PresentationReconciler;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.IMemento;
import org.eclipse.ui.IPartListener;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.IViewSite;
import org.eclipse.ui.IWorkbenchActionConstants;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.actions.ActionFactory;
import org.eclipse.ui.actions.ActionFactory.IWorkbenchAction;
import org.eclipse.ui.operations.RedoActionHandler;
import org.eclipse.ui.operations.UndoActionHandler;
import org.eclipse.ui.part.ViewPart;
import org.eclipse.ui.texteditor.IUpdate;

// TODO: Change the save location? Save in the user's home directory?

// TODO: bold, italic

// TODO: bullet list? task list, w/ finished / unfinished items?

// TODO: find dialog (see FindReplaceAction, TextConsolePage)

// TODO: shift left and right

// TODO: print?

// TODO: auto-indent strategy?

// TODO: We lost underlining of urls - fix it.

// TODO: dirty flag should use the operations history


/**
 * This view acts like a simple scratch-pad like text editor. It is similar to the old Macintosh
 * Sitckies application. The user never needs to explicitly save the note text; it is automatically
 * saved and restored on application termination and startup.
 * 
 * @author Devon Carew
 */
public class NotesView
	extends ViewPart
{
	private static final String 	TOP_LINE_INDEX = "topLineIndex";
	private static Color			NOTE_COLOR;
	
	private TextViewer			textViewer;
	private IMemento			memento;
	
	private Action				action1;
	private Action				exportAction;
	
	private boolean 			dirty;
	
	private UndoActionHandler 	undoAction;
	private RedoActionHandler 	redoAction;
	private IUndoContext 		undoContext;
	
	private Map					textActions = new HashMap();
	
	/** We listen for part deactivated events and perform an auto-save. */
	private IPartListener		partListener = new IPartListener() {
		public void partActivated(IWorkbenchPart part) {
			
		}
		public void partBroughtToTop(IWorkbenchPart part) {
			
		}
		public void partClosed(IWorkbenchPart part) {
			
		}
		public void partDeactivated(IWorkbenchPart part) {
			if (part == NotesView.this)
				saveNotesText();
		}
		public void partOpened(IWorkbenchPart part) {
			
		}
	};
	
	
	/**
	 * Create a new Notes instance.
	 */
	public NotesView()
	{
		
	}
	
	public Object getAdapter(Class adapter)
	{
		if (IFindReplaceTarget.class.equals(adapter))
			return textViewer == null ? null : textViewer.getFindReplaceTarget();
		
		if (ITextOperationTarget.class.equals(adapter))
			return textViewer == null ? null : textViewer.getTextOperationTarget();

		if (IRewriteTarget.class.equals(adapter))
			return ((ITextViewerExtension) textViewer).getRewriteTarget();
		
		if (Control.class.equals(adapter))
			return textViewer != null ? textViewer.getTextWidget() : null;
		
		return super.getAdapter(adapter);
	}
	
	/**
	 * This is a callback that will allow us to create the viewer and initialize it.
	 */
	public void createPartControl(Composite parent)
	{
		if (NOTE_COLOR == null)
			NOTE_COLOR = new Color(parent.getDisplay(), 255, 255, 180);
		
		GridLayout layout = new GridLayout(1, false);
		layout.marginLeft = 3;
		layout.marginWidth = 0;
		layout.marginHeight = 0;
		parent.setLayout(layout);
		
		parent.setBackground(NOTE_COLOR);
		
		textViewer = new TextViewer(parent, SWT.MULTI | SWT.WRAP | SWT.V_SCROLL);
		textViewer.getControl().setFont(JFaceResources.getFont(JFaceResources.TEXT_FONT));
		textViewer.getControl().setBackground(NOTE_COLOR);
		textViewer.getControl().setLayoutData(new GridData(GridData.FILL_BOTH));
		textViewer.setDocument(new Document());
		textViewer.setHyperlinkPresenter(new DefaultHyperlinkPresenter(new RGB(0, 0, 255)));
		textViewer.setHyperlinkDetectors(new IHyperlinkDetector[] { new URLHyperlinkDetector() }, SWT.MOD1);
		textViewer.setUndoManager(new TextViewerUndoManager(100));
		textViewer.setTextDoubleClickStrategy(new DefaultTextDoubleClickStrategy(), IDocument.DEFAULT_CONTENT_TYPE);
		textViewer.getDocument().addDocumentListener(new IDocumentListener() {
			public void documentAboutToBeChanged(DocumentEvent event) {
				
			}
			public void documentChanged(DocumentEvent event) {
				setDirty(true);
				updateActions();
			}
		});
		textViewer.addSelectionChangedListener(new ISelectionChangedListener() {
			public void selectionChanged(SelectionChangedEvent event) {
				updateActions();
			}
		});
		
		StyledText styleText = textViewer.getTextWidget();
		styleText.setEditable(true);
		styleText.setEnabled(true);
		styleText.setTabs(4);
		
		PresentationReconciler presentationReconciler = new PresentationReconciler();
		presentationReconciler.install(textViewer);
		
		// Enable the TextViewerUndoManager.
		textViewer.activatePlugins();
		
		makeActions();
		hookContextMenu();
		createGlobalActionHandlers();
		contributeToActionBars();
		
		loadNotesText();
		
		restoreUISettings(this.memento);
		
		updateActions();
	}
	
	private void hookContextMenu()
	{
		MenuManager menuMgr = new MenuManager("#PopupMenu");
		
		menuMgr.setRemoveAllWhenShown(true);
		menuMgr.addMenuListener(new IMenuListener() {
			public void menuAboutToShow(IMenuManager manager) {
				NotesView.this.fillContextMenu(manager);
			}
		});
		
		Menu menu = menuMgr.createContextMenu(textViewer.getControl());
		textViewer.getControl().setMenu(menu);
		getSite().registerContextMenu(menuMgr, textViewer);
	}
	
	private void contributeToActionBars()
	{
		IActionBars bars = getViewSite().getActionBars();
		
		fillLocalToolBar(bars.getToolBarManager());
	    fillLocalPullDown(bars.getMenuManager());
	}
	
	private void fillContextMenu(IMenuManager manager)
	{
		manager.add(getAction(ActionFactory.CUT.getId()));
		manager.add(getAction(ActionFactory.COPY.getId()));
		manager.add(getAction(ActionFactory.PASTE.getId()));
		manager.add(new Separator());
		manager.add(getAction(ActionFactory.DELETE.getId()));
		manager.add(getAction(ActionFactory.SELECT_ALL.getId()));
//		manager.add(new Separator());
//		manager.add(getAction(ActionFactory.FIND.getId()));
		
		// Other plug-ins can contribute there actions here
		manager.add(new Separator(IWorkbenchActionConstants.MB_ADDITIONS));
	}
	
	private void fillLocalToolBar(IToolBarManager manager)
	{
		//manager.add(action1);
	}
	
	private void fillLocalPullDown(final IMenuManager manager)
    {
       manager.add(exportAction);
    }
    
	private void createGlobalActionHandlers()
	{
		undoContext = ((IUndoManagerExtension)textViewer.getUndoManager()).getUndoContext();
		
		// set up action handlers that operate on the current context
		undoAction = new UndoActionHandler(getSite(), undoContext);
		redoAction = new RedoActionHandler(getSite(), undoContext);
		
		IActionBars actionBars = getViewSite().getActionBars();
		
		actionBars.setGlobalActionHandler(ActionFactory.UNDO.getId(), undoAction);
		actionBars.setGlobalActionHandler(ActionFactory.REDO.getId(), redoAction);
		
		// Install the standard text actions.
		addTextAction(ActionFactory.CUT, ITextOperationTarget.CUT);
		addTextAction(ActionFactory.COPY, ITextOperationTarget.COPY);
		addTextAction(ActionFactory.PASTE, ITextOperationTarget.PASTE);
		addTextAction(ActionFactory.DELETE, ITextOperationTarget.DELETE);
		addTextAction(ActionFactory.SELECT_ALL, ITextOperationTarget.SELECT_ALL);
	}
	
	private void updateActions()
	{
		for (Iterator itor = textActions.values().iterator(); itor.hasNext(); )
		{
			((IUpdate)itor.next()).update();
		}
	}
	
	protected void addTextAction(ActionFactory actionFactory, int textOperation)
	{
		IWorkbenchWindow window = getViewSite().getWorkbenchWindow();
		IWorkbenchAction globalAction = actionFactory.create(window);
		
		// Create our text action.
		TextViewerAction textAction = new TextViewerAction(textOperation);
		
		textActions.put(actionFactory.getId(), textAction);
		
		// Copy its properties from the global action.
		textAction.setText(globalAction.getText());
		textAction.setToolTipText(globalAction.getToolTipText());
		textAction.setDescription(globalAction.getDescription());
		textAction.setImageDescriptor(globalAction.getImageDescriptor());
		textAction.setDisabledImageDescriptor(globalAction.getDisabledImageDescriptor());
		textAction.setAccelerator(globalAction.getAccelerator());
		
		// Make sure it's up to date.
		textAction.update();
		
		// Register our text action with the global action handler.
		IActionBars actionBars = getViewSite().getActionBars();
		
		actionBars.setGlobalActionHandler(actionFactory.getId(), textAction);
	}
	
	protected TextViewerAction getAction(String actionID)
	{
		return (TextViewerAction)textActions.get(actionID);
	}
	
	class TextViewerAction
		extends Action
		implements IUpdate
	{
		private int actionId;
		
		TextViewerAction(int actionId)
		{
			this.actionId = actionId;
		}
		
		public boolean isEnabled()
		{
			return textViewer.canDoOperation(actionId);
		}
		
		public void run()
		{
			textViewer.doOperation(actionId);
			
			updateActions();
		}
		
		public void update()
		{
			if (super.isEnabled() != isEnabled())
				setEnabled(isEnabled());
		}
	}
	
	private void makeActions()
	{
		action1 = new Action() {
			public void run() {
				showMessage("Action 1 executed");
			}
		};
		action1.setText("Action 1");
		action1.setToolTipText("Action 1 tooltip");
		action1.setImageDescriptor(
			PlatformUI.getWorkbench().getSharedImages().getImageDescriptor(ISharedImages.IMG_OBJS_INFO_TSK));
		
		exportAction = createExportAction();
	}
	
	private void showMessage(String message)
	{
		MessageDialog.openInformation(textViewer.getControl().getShell(), "Notes", message);
	}
	
	/**
	 * Passing the focus request to the viewer's control.
	 */
	public void setFocus()
	{
		textViewer.getControl().setFocus();
	}
	
	public void init(IViewSite site, IMemento memento)
		throws PartInitException
	{
		super.init(site, memento);
		
		this.memento = memento;
		
		site.getPage().addPartListener(partListener);
	}
	
	public void dispose()
	{
		getSite().getPage().removePartListener(partListener);
		
		saveNotesText();
		
		super.dispose();
	}
	
	public void saveState(IMemento memento)
	{
		saveUISettings(memento);
		
		super.saveState(memento);
	}
	
	private void saveUISettings(IMemento memento)
	{
		memento.putInteger(TOP_LINE_INDEX, textViewer.getTopIndex());
	}
	
	private void restoreUISettings(IMemento memento)
	{
		if (memento == null)
			return;
		
		Integer topLineIndex = memento.getInteger(TOP_LINE_INDEX);
		
		// TODO: This plus one is due to a bug where getting / setting the top line index is off by one.
		if (topLineIndex != null && topLineIndex.intValue() != -1)
			textViewer.setTopIndex(topLineIndex.intValue() + 1);
	}
	
	private IPath getDataPath()
	{
		IPath pluginDir = NotesPlugin.getDefault().getStateLocation();
		
		return pluginDir.append("notes.txt");
	}
	
	public boolean isDirty()
	{
		return dirty;
	}
	
	private void setDirty(boolean dirty)
	{
		if (this.dirty != dirty)
			this.dirty = dirty;
	}
	
	private void loadNotesText()
	{
		IPath 	dataPath = getDataPath();
		File 	file = dataPath.toFile();
		
		if (!file.exists() || !file.canRead())
			return;
		
		try
		{
			String text = NotesFileUtils.readTextUTF8(file);
			
			if (text != null)
			{
				textViewer.getDocument().set(text);
				
				setDirty(false);
				
				// Reset undo history.
				textViewer.resetPlugins();
			}
		}
		catch (IOException ioe)
		{
			NotesPlugin.log(ioe);
		}
	}
	
	private void saveNotesText()
	{
		if (!isDirty())
			return;
		
		IPath 	dataPath = getDataPath();
		File 	file = dataPath.toFile();
		
		String 	text = textViewer.getDocument().get();
		
		try
		{
			NotesFileUtils.writeTextUTF8(text, file);
			
			setDirty(false);
		}
		catch (IOException ioe)
		{
			NotesPlugin.log(ioe);
		}
	}
	
	private Action createExportAction()
	{
		Action action = new Action() {
			public void run() {
				FileDialog saveDialog = new FileDialog(getSite().getShell(), SWT.SAVE);
				saveDialog.setFilterExtensions(new String[] { "*.txt" });
				
				String absPathName = saveDialog.open();
				
				if (absPathName != null)
				{
					try
					{
						String text = textViewer.getDocument().get();
						
						NotesFileUtils.writeTextUTF8(text, new File(absPathName));
					}
					catch (IOException ioe)
					{
						MessageDialog.openError(getSite().getShell(),
							"Error Exporting Text", "Unable to export Notes text: " + ioe.getCause());
					}
				}
			}
		};
		
		action.setText("Export...");
		action.setToolTipText("Export the Notes Text");
		action.setImageDescriptor(NotesPlugin.getImageDescriptor("icons/etool16/importdir_wiz.gif"));
		
		return action;
	}
	
}
